// Package dao
/**
* @Project : geektime-basic-go-study
* @File    : user.go
* @IDE     : GoLand
* @Author  : Tvux
* @Date    : 2024/9/5 13:05
**/

package dao

// dao 存储数据库的抽象

import (
	"context"
	"database/sql"
	"errors"
	"github.com/go-sql-driver/mysql"
	"gorm.io/gorm"
	"time"
)

var (
	ErrUserDuplicate = errors.New("用户冲突")
	ErrUserNotFound  = gorm.ErrRecordNotFound
)

// User ↓直接对应于数据库表结构
// 有些人叫 entity，或者 model，或者 PO（Persistent Object）
type User struct {
	Id int64 `gorm:"primaryKey, autoIncrement"` // 自增主键
	// 设置为唯一索引
	Email    sql.NullString `gorm:"unique"`
	Password string
	// 如果很多用户都用邮箱登录，此时手机号都没有设置过，都为空串""
	//Phone string `gorm:"unique"`
	// 此时给 Phone 加的唯一索引就会冲突，所以 Email 跟 Phone 应该是可以为空的，sql.NullString
	// 唯一索引允许有多个空值，但是不能有多个 ""
	// 下面这种做法不是go推荐的做法，但是也可以，主要问题是需要解引用，并且判空指针
	//Phone *string `gorm:"unique"`
	Phone    sql.NullString `gorm:"unique"`
	Nickname sql.NullString
	// 生日，毫秒数
	Birthday sql.NullInt64
	// 自我介绍
	AboutMe sql.NullString `gorm:"type:varchar(1024)"`
	// 微信
	WechatOpenId  sql.NullString `gorm:"unique"`
	WechatUnionId sql.NullString
	// 创建时间，毫秒数
	// 存储毫秒数，可以很好解决时区问题，后端中均使用毫秒数进行时间的计算，涉及到时区的再进行转化
	CTime int64
	// 更新时间，毫秒数
	UTime int64
}

type UserDAO interface {
	Insert(ctx context.Context, u User) error
	UpdateNonZeroFields(ctx context.Context, u User) error
	FindUserByEmail(ctx context.Context, email string) (User, error)
	FindUserById(ctx context.Context, id int64) (User, error)
	FindUserByPhone(ctx context.Context, phone string) (User, error)
	FindUserByWechat(ctx context.Context, openId string) (User, error)
}

var (
	_ UserDAO = (*GormUserDAO)(nil)
)

type GormUserDAO struct {
	db *gorm.DB
}

func NewGormUserDAO(db *gorm.DB) UserDAO {
	return &GormUserDAO{
		db: db,
	}
}

func (Self *GormUserDAO) Insert(ctx context.Context, u User) error {
	now := time.Now().UnixMilli()
	// SELECT * FROM users WHERE email=123@qq.com FOR UPDATE // for update 会锁住
	// 根据数据库的级别，来设置锁，比如间隙锁，可重复读级别会设置间隙锁，锁住一段，此时插入是插不进去的
	u.CTime = now
	u.UTime = now
	err := Self.db.WithContext(ctx).Create(&u).Error
	var mysqlErr *mysql.MySQLError
	if errors.As(err, &mysqlErr) {
		const uniqueConflictsErrNo uint16 = 1062
		if mysqlErr.Number == uniqueConflictsErrNo {
			// 出现了唯一索引冲突，可能是邮箱冲突，也可能是手机号冲突
			return ErrUserDuplicate
		}
	}
	return err
}

func (Self *GormUserDAO) UpdateNonZeroFields(ctx context.Context, u User) error {
	// 这个函数依赖于 GORM Updates 的两个默认行为：
	// 1、默认使用记录的 ID 字段作为 WHERE 条件来确定要更新的数据库记录。
	// 2、只更新那些非零值字段。
	// 另外一种做法是显式指定只更新必要的字段，
	// 那么这意味着 DAO 和 service 中非敏感字段语义耦合了
	return Self.db.WithContext(ctx).Updates(&u).Error
}

func (Self *GormUserDAO) FindUserByEmail(ctx context.Context, email string) (User, error) {
	var u User
	err := Self.db.WithContext(ctx).Where("email = ?", email).First(&u).Error
	return u, err
}

func (Self *GormUserDAO) FindUserById(ctx context.Context, id int64) (User, error) {
	var u User
	err := Self.db.WithContext(ctx).Where("id = ?", id).First(&u).Error
	return u, err
}

func (Self *GormUserDAO) FindUserByPhone(ctx context.Context, phone string) (User, error) {
	var u User
	err := Self.db.WithContext(ctx).Where("phone = ?", phone).First(&u).Error
	return u, err
}

func (Self *GormUserDAO) FindUserByWechat(ctx context.Context, openId string) (User, error) {
	var u User
	err := Self.db.WithContext(ctx).Where("wechat_open_id = ?", openId).First(&u).Error
	return u, err
}
